# Seengreat MCP23017 IO Expansion Module control demo
# MicroPython demo for Raspberry Pi Pico + MCP23017
# - PORTA: OUTPUT, toggles all pins high/low every 1s
# - PORTB: INPUT with pull-ups, interrupt on change using INTB pin
# Web Site  : www.seengreat.com
# Raspberry Pi Pico Wiring example:
#   Pico GP0  (I2C0 SDA) -> MCP23017 SDA
#   Pico GP1  (I2C0 SCL) -> MCP23017 SCL
#   Pico 3V3             -> MCP23017 VCC
#   Pico GND             -> MCP23017 GND
#   Pico GP2             -> MCP23017 INTB
#   Pico GP3             -> MCP23017 INTA (optional)

from machine import Pin, I2C
import utime

# CONTROL REGISTER in Sequential mode IOCON.BANK = 0
IODIR    = 0x00
IPOL     = 0x02
GPINTEN  = 0x04
DEFVAL   = 0x06
INTCON   = 0x08
IOCON    = 0x0A
GPPU     = 0x0C
INTF     = 0x0E
INTCAP   = 0x10
GPIO     = 0x12
OLAT     = 0x14

# State definition
INPUT  = 0x01
OUTPUT = 0x00

# Port definition
PORTA  = 0x00
PORTB  = 0x01

# Pin bit masks
PIN0    = 0x01
PIN1    = 0x02
PIN2    = 0x04
PIN3    = 0x08
PIN4    = 0x10
PIN5    = 0x20
PIN6    = 0x40
PIN7    = 0x80
PIN_ALL = 0xFF

ENABLE  = 0x01
DISABLE = 0x00

HIGH = 0x01
LOW  = 0x00

# INTERRUPT type definition
INT_DISABLE       = 0x00
INT_HIGH_LEVEL    = 0x01
INT_LOW_LEVEL     = 0x02
INT_CHANGE_LEVEL  = 0x03

# Define whether INTA and INTB are associated
INTAB_CONJUNCTION = 0x00
INTAB_INDEPENDENT = 0x01

# Define the output type of INTA and INTB pins (IOCON bits in original demo)
INT_OD             = 0x00
INT_PUSHPULL_HIGH  = 0x01
INT_PUSHPULL_LOW   = 0x02


class MCP23017:
    def __init__(self, i2c, addr=0x27, pin_inta=None, pin_intb=None):
        self.i2c = i2c
        self.addr = addr

        # Optional interrupt pins on Pico
        self.INTA = Pin(pin_inta, Pin.IN, Pin.PULL_UP) if pin_inta is not None else None
        self.INTB = Pin(pin_intb, Pin.IN, Pin.PULL_UP) if pin_intb is not None else None

        # Default directions
        self.set_port_dir(PORTA, INPUT)
        self.set_port_dir(PORTB, INPUT)

        # Reset other registers to 0x00 (same as original: 2..21)
        for reg in range(2, 22):
            self.write_reg(reg, 0x00)

        # Config INTA/INTB independent, output type push-pull high (per original demo)
        self.write_reg(IOCON, INTAB_INDEPENDENT | INT_PUSHPULL_HIGH)

    def write_reg(self, reg, value):
        self.i2c.writeto_mem(self.addr, reg, bytes([value & 0xFF]))

    def read_reg(self, reg):
        return self.i2c.readfrom_mem(self.addr, reg, 1)[0]

    def set_port_dir(self, port, port_dir):
        if port_dir == INPUT:
            self.write_reg(IODIR + port, 0xFF)
        elif port_dir == OUTPUT:
            self.write_reg(IODIR + port, 0x00)

    def set_io_dir(self, port, pin, pin_dir):
        state = self.read_reg(IODIR + port)
        if pin_dir == INPUT:
            state |= pin
        elif pin_dir == OUTPUT:
            state &= (~pin) & 0xFF
        else:
            return
        self.write_reg(IODIR + port, state)

    def set_io_pu(self, port, pin, pu):
        state = self.read_reg(GPPU + port)
        if pu == ENABLE:
            state |= pin
        elif pu == DISABLE:
            state &= (~pin) & 0xFF
        else:
            return
        self.write_reg(GPPU + port, state)

    def set_io_polarty(self, port, pin, polarity):
        state = self.read_reg(IPOL + port)
        if polarity == ENABLE:
            state |= pin
        elif polarity == DISABLE:
            state &= (~pin) & 0xFF
        else:
            return
        self.write_reg(IPOL + port, state)

    def set_io_int(self, port, pin, int_type):
        inten_state  = self.read_reg(GPINTEN + port)
        defval_state = self.read_reg(DEFVAL + port)
        intcon_state = self.read_reg(INTCON + port)

        if int_type == INT_DISABLE:
            inten_state &= (~pin) & 0xFF

        elif int_type == INT_HIGH_LEVEL:
            inten_state |= pin
            defval_state &= (~pin) & 0xFF
            intcon_state |= pin

        elif int_type == INT_LOW_LEVEL:
            inten_state |= pin
            defval_state |= pin
            intcon_state |= pin

        elif int_type == INT_CHANGE_LEVEL:
            inten_state |= pin
            intcon_state &= (~pin) & 0xFF

        self.write_reg(GPINTEN + port, inten_state)
        self.write_reg(DEFVAL + port, defval_state)
        self.write_reg(INTCON + port, intcon_state)

    def read_intf(self, port):
        return self.read_reg(INTF + port)

    def read_intcap(self, port):
        return self.read_reg(INTCAP + port)

    def read_gpio(self, port):
        return self.read_reg(GPIO + port)

    def read_olat(self, port):
        return self.read_reg(OLAT + port)

    def write_gpio(self, port, value):
        self.write_reg(GPIO + port, value & 0xFF)

    def set_gpio_pin(self, port, pin, value):
        gpio_state = self.read_reg(GPIO + port)
        if value == HIGH:
            gpio_state |= pin
        elif value == LOW:
            gpio_state &= (~pin) & 0xFF
        self.write_reg(GPIO + port, gpio_state)


# ---------------- Main ----------------

# Choose Pico I2C pins (edit to match your wiring)
I2C_ID = 0
SDA_PIN = 0   # GP0
SCL_PIN = 1   # GP1

# Choose interrupt pins on Pico (edit to match your wiring)
INTB_PIN = 2  # GP2 connected to MCP23017 INTB
INTA_PIN = 3  # GP3 connected to MCP23017 INTA (optional)

# MCP23017 address (A2 A1 A0 = 1 1 1 -> 0x27)
MCP_ADDR = 0x27

i2c = I2C(I2C_ID, sda=Pin(SDA_PIN), scl=Pin(SCL_PIN), freq=400000)

# (Optional) quick scan
print("I2C scan:", [hex(x) for x in i2c.scan()])

mcp = MCP23017(i2c, addr=MCP_ADDR, pin_inta=INTA_PIN, pin_intb=INTB_PIN)

# PORTA output, PORTB input
mcp.set_port_dir(PORTA, OUTPUT)
mcp.set_port_dir(PORTB, INPUT)

# PORTB enable pullups
mcp.set_io_pu(PORTB, PIN_ALL, ENABLE)

# Interrupt on any change on PORTB
mcp.set_io_int(PORTB, PIN_ALL, INT_CHANGE_LEVEL)

# Interrupt callback for INTB
def intb_callback(pin):
    # On MCP23017, reading INTCAP clears the interrupt condition
    portb_gpio = mcp.read_gpio(PORTB)
    portb_cap  = mcp.read_intcap(PORTB)
    portb_gpio2 = mcp.read_gpio(PORTB)

    print("INTB IRQ!")
    print("PORTB GPIO :", hex(portb_gpio))
    print("PORTB INTCAP:", hex(portb_cap))
    print("PORTB GPIO2:", hex(portb_gpio2))

# Attach IRQ:
# Many MCP23017 boards default to active-low interrupt output; falling edge is common.
# If your board is active-high, change to Pin.IRQ_RISING.
if mcp.INTB is not None:
    mcp.INTB.irq(trigger=Pin.IRQ_FALLING, handler=intb_callback)

# Print initial state
print("PORTB INTCAP:", hex(mcp.read_intcap(PORTB)))
print("PORTB GPIO  :", hex(mcp.read_gpio(PORTB)))

# Toggle all pins on PORTA
while True:
    print("PORTB GPIO:", hex(mcp.read_gpio(PORTB)))
    print("PORTA -> HIGH")
    mcp.write_gpio(PORTA, 0xFF)
    utime.sleep(1)

    print("PORTA -> LOW")
    mcp.write_gpio(PORTA, 0x00)
    utime.sleep(1)

